fix(platform-objects): decide the report/dataset panel en-echoes per leaf, and pin the decisions as a ledger - #19401
Conversation
… card decided Twelve label leaves and nine helpText leaves at the four keys #19355 names shipped byte-identical to their `en` source in `zh-CN`, `ja-JP` and `es-ES`. Each was judged per leaf; every one is an unauthored extractor fill, not a rendering decision, and the evidence is per key in the PR body. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
…ables `pnpm i18n:extract` — the command this package's extract config documents. It rewrote all eleven bundles; the three translated `metadata-forms` bundles round-tripped byte-identical, and the only change is the 21 provenance rows for the leaves the previous commit translated, which the extractor drops because those leaves are no longer byte copies of their `en` source. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
…s a ledger One row per string leaf of the four keys #19355 names, each carrying the verdict and the reason it was reached. The population is a list because the neighbouring derived pin cannot take these keys: widening its 124 enumerated repeater row properties to all 417 declared form-field paths lands red on 230 echo leaves across 82 keys nobody has decided yet. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
…ted panel leaves Measured rather than assumed: the package is not `private`, `files[]` ships `dist`, and the new strings reach five published entry points there (`index.js`/`index.mjs`, `plugin.js`/`plugin.mjs`, `metadata-translations/index.*`) — the same five a pre-existing translated leaf on the same panel reaches. So the diff publishes and `skip-changeset` would be wrong. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a90fa2f4277b9cd3427152f2c1f257b3d4b138b8 && git checkout a90fa2f4277b9cd3427152f2c1f257b3d4b138b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fade3da23eb4c5ce580bac64e4495832e30bac8d 68049082b9db68def033820be0b7887b373c8205 && git checkout -B drift-repro fade3da23eb4c5ce580bac64e4495832e30bac8d && git merge --no-ff 68049082b9db68def033820be0b7887b373c8205
node scripts/docs-audit/affected-docs.mjs --json fade3da23eb4c5ce580bac64e4495832e30bac8d |
… per leaf, and derive the panel pin (objectstack-ai#19418) Part of objectstack-ai#19403 Clause-②: no ## What this is One panel family, decided **leaf by leaf**. The card's deliverable is a per-leaf judgement with its reason recorded, not 84 translations; every leaf below was decided on its own evidence, and the verdicts with their reasons live in a ledger the suite reads (`object-field-editor-panel-echo-decisions.test.ts`), not only in this body. **The family: the object field editor** — `object.fields['fields.*']`, the panel the card samples (`placeholder`, `valueDomain`, `rows`, `deleteBehavior`, `expression` …). It is the most trafficked authoring surface in Studio: every field an author creates is typed into it. ⛔ Not `Fixes` — this is one family of the card's population, and the card outlives this round. **Eight families remain** (65 label keys), listed at the foot of this body. ## The reading, re-taken on this base — the card's numbers are NOT inherited Census instrument: flatten every `*.metadata-forms.generated.ts` bundle and compare `.label` leaves against the `en` bundle. Population fixed here: **the `.label` leaves of the metadata-form catalogs** (the seat's population, 538 leaves), **plus the sibling `helpText` leaf of every key this round decides** — a panel whose field name is Chinese and whose tooltip is English is the same defect half fixed. Measured on worktree base `0862063ba`: | reading | result | |:--|:--| | `en` string leaves in the metadata-form catalogs | 893 | | of which `.label` leaves | 538 | | ⭐ POSITIVE CONTROL — labels genuinely translated | **459** (`zh-CN`) · **443** (`ja-JP`) · **443** (`es-ES`) | | labels echoing the `en` source, per locale | 79 · 95 · 95 | | label keys echoing in ALL THREE locales | **79** ⇒ 237 leaves | ⭐ The control is what makes the echo count mean something: 459 translated against 79 echoing is a discrimination, not a parser that matches everything. **This is 79 / 237, not the card's 83 / 249, and the difference is fully accounted for**: the card's table was taken before PR objectstack-ai#19401 landed, which translated exactly four label leaves (`report.fields.dataset` / `values` / `rows`, `dataset.fields.measures`). 83 − 4 = 79, and the control moves the same way (455 → 459, 439 → 443, 439 → 443), matching the reading the dispatching seat took after objectstack-ai#19401 to the unit. The other count on the card (82 / 230, over 417 declared form-field paths, older base) answers a different question and is not comparable — stated there, confirmed here. **After this change**: 65 keys / 195 leaves echo in all three locales; control 473 · 457 · 457. ## The verdicts — 14 keys × 2 string leaves × 3 locales = 84 decisions Every one is `translate`, and **not one of them is "translate because it is English"**. The echo direction was tested per leaf by asking what this catalog's own authored corpus answers for that term: - **Same-string precedent, authored in all three locales** — `placeholder` (`action.fields.params.placeholder.label` reads 占位文本 / プレースホルダー / Marcador de posición), `deleteBehavior` and `expression` and `summaryOperations` (all three authored on the sibling `field` panel, label and helpText), `summaryOperations.object` / `.field` (nine and seven other paths), `visibleWhen` (two other paths). - **Decided AGAINST a same-string precedent** — `fields.rows.label`. `report.fields.rows.label` is authored as 行 / 行 / Filas, but that leaf names a pivot axis and this one is a COUNT of text rows (its own helpText says so), so `zh-CN` and `ja-JP` depart to 行数 while `es-ES` keeps Filas, which carries both senses. This is the leaf that shows the judgement is per leaf and not per string. - **Machine tokens kept English inside translated prose** — `lookup` in `lookupFilters` (the authored `fields.reference.helpText` keeps `lookup` and `tree` in all three locales), `count` in `summaryOperations.field.helpText`, `CEL`, and the autonumber literals `"INV-{0000}"` / `{YYYY}/{MM}/{DD}` / `{field_name}`. - **A distinction the source draws, preserved** — `summaryOperations.function.label` is rendered as the plain word (函数 / 関数 / Función), deliberately not as "aggregation function", because the source says `Function` at the label and `Aggregation function` at the helpText one line below. - **Panel-internal control** for the prose leaves: each carried an entry in this package's per-locale `source-hashes.generated.ts` provenance table in all three locales, while every authored neighbour on the same panel carried none — which is what an unauthored extractor fill looks like here. Per-leaf reasons for all 28 leaves are in the ledger; this list is the summary, not the record. ## The instrument, reused not reinvented `object-field-editor-panel-echo-decisions.test.ts` is the objectstack-ai#19355 shape: one row per leaf, verdict per locale, reason, and the `en` source pinned EQUAL to the live bundle so rewording the English reds the row instead of leaving a decision standing over text nobody judged. An `echo` verdict must carry its own per-locale departure reason. Two additions, both because this round would otherwise ship a check that cannot fail: 1. **The echo-reason rule is a predicate with a dark control.** Every verdict here is `translate`, so asserting the rule over the ledger alone evaluates nothing — the suite feeds it a synthetic `echo` row and asserts it is refused, then asserts it is accepted once a departure reason is added. 2. **The panel population is DERIVED, which is the structural half the card asks for.** `repeater-row-properties.test.ts` already refuses an en-echo inside its population, and it filters on `spec.type === 'repeater'`; the object form declares its field editor as `{ field: 'fields', type: 'record' }` with 35 children, so that derivation yields exactly four row properties for `objectForm` (`fields.options.*`) and **none** of the 14 keys here — the card's "0 of the keys sit inside it", measured rather than assumed. The new file walks the `object.fields['fields.*']` leaves out of the `en` catalog and refuses any that reads its source without a ledger row saying so, so a re-fill or a field added to the editor tomorrow is red on the day it lands. ## Regeneration — the silent-loss hazard on this path Translated-locale values are hand-written; the provenance tables are not. Regenerated with **`pnpm i18n:extract`** (the command this package's extract config documents), never by hand, and **no key was added or removed** — so the `merge=os-regen` gap on `*.metadata-forms.generated.ts` (`git check-attr merge` reads `unspecified` on these paths) has nothing to lose even in principle. The bundle diff is 84 insertions / 84 deletions, leaf for leaf; the extract's own diff is 84 deletions and zero insertions — the 28 provenance rows per locale that now record these leaves as authored. `pnpm check:i18n` green is the proof that the hand-written values are byte-identical to what a fresh extract writes, across all nine bundle sets. ## Reverse verification — the new pin can fail Committed first, then ablated through `scripts/ablation-replace.mjs` (anchor must hit; on-disk counts and blob hashes are the tool's own verdict), one decided leaf reverted to its English source in `zh-CN`: - before: `label: "占位文本",` x1 · after: x0, `label: "Placeholder",` x1 → x2, blob `8f480a6bbe30` → `6bd6a8c0b277` - suite under the mutation: **2 failed | 9 passed** — the enumerated verdict (`zh-CN object.fields.fields.placeholder.label reads its en source again`) AND the derived panel pin, independently - restored: blob back to `8f480a6bbe30` == HEAD, `git diff HEAD` empty, working tree clean - suite restored: **11 passed** ## Verification - `pnpm --filter @objectstack/platform-objects test` — 44 files, **610 passed** - `pnpm --filter @objectstack/platform-objects typecheck` — green (its `tsconfig.test.json` program reaches the new file; `--listFiles` confirms it, and it contributes no errors) - `pnpm check:i18n` — **green**, 9 packages in sync (prerequisite closure built first; its first run exited **3**, PREREQUISITE NOT MET, which is not a reading) - `pnpm lint` — repo-wide, **exit 0**, at `da144a4d` - Gate families derived with `scripts/pm/dispatch-gates.mjs` and reconciled with `--ran`: **59 derived · 58 run green · 0 unrun · 1 NOT MEASURED** — `pnpm check:dual-build-cjs-loads` exits 3 (PREREQUISITE NOT MET: it reads built output for every package in the repo). That prerequisite is a whole-repo build, which is CI's `Build Core`, and this diff adds no package and changes no export. ## Changeset, measured `@objectstack/platform-objects` is `private: false` with `files: ["dist", …]`, and `src/metadata-translations/index.ts` imports all three translated bundles. Probed on the built output: a new leaf (`Dominio de valores`, and 占位文本 in its esbuild-escaped form) is present in `dist/metadata-translations/index.js`, `dist/index.js` and `dist/plugin.js`, with a pre-existing translated leaf as the positive control and an invented string as the negative control. ⇒ the diff publishes, `skip-changeset` would be wrong, and a **`patch`** changeset is included.⚠️ Worth one line for whoever probes this dist next: the first probe searched for the literal CJK and found **nothing, including the positive control** — tsup emits non-ASCII as `\uXXXX`. A probe with no positive control would have read that as "this does not publish". ## The eight families that remain (65 label keys, all-three-locale echoes, this base) | family | keys | |:--|--:| | `page.fields.interfaceConfig.*` (+ `page.sections.interface`) | 15 | | `object.fields.enable.*` (+ `validations`) — the object capabilities panel | 9 | | `field.fields.*` — the standalone field panel (several keys now have a precedent from this round) | 9 | | `dataset.fields.*` + `dataset.sections.*` + `dataset.label` | 12 | | `action.fields.*` | 6 | | `hook.fields.*` | 5 | | metadata-type display names with no panel: `seed` · `mapping` · `api` · `doc` · `book` · `capability` | 6 | | `report.fields.*` + `report.sections.dataset_binding` | 3 | ## Acceptance notes Noted, not filed: - `report.sections.*` (label and description) — the population the card records as having no home. Still uncounted by any `.label` census and unseen by any pin. Successor: the `report` family row above, if the seat dispatches it. - The object form declares `deleteBehavior` **twice** under its field editor, with mutually exclusive `visibleWhen` predicates (`data.type == 'lookup'` and `data.type == 'master_detail'`) and different option sets. Checked because a duplicate child looked like a defect: it is deliberate, both copies carry the same label and helpText, and the extractor emits one key. No finding. - `fields.lookupFilters` declares no `type` (it carries `widget: 'json'`). Legal — `FormFieldBaseSchema.type` is `.optional()` with "auto-infers widget if omitted". No finding. - The nine bundle sets other than this one still do not opt into the provenance table (`--source-hashes`), so their generated leaves stay legacy-trusted. Documented in the extract config; not this card's. --- _Generated by [Claude Code](https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19355
Clause-②: no
What this is
The card's deliverable is a per-leaf judgement with its reason recorded, not twelve translations. Every leaf below was decided on its own evidence; the verdicts and their reasons live in a ledger the suite reads (
report-dataset-panel-echo-decisions.test.ts), not only in this body.The reading, re-taken on this base
Re-derived mechanically on the worktree base
3ff6ddf1e, over the four keys the card names, inzh-CN/ja-JP/es-ES:ensource — the card's count is CONFIRMED, key for key and locale for locale.helpTextleaves at the same four keys were also byte copies (report.fields.dataset/values/rows, three locales each). They are the same authored unit as the label they sit on, so a fix that left them English would leave a panel whose field name is Chinese and whose tooltip is not.dataset.fields.measures.helpTextwas already translated in all three locales. That leaf is this change's negative control and is recorded as such.⇒ decided population: 4 keys × 8 string leaves × 3 locales = 24 decisions, of which 21 were echoes and 3 were already authored.
The instrument, with both controls
This package ships its own provenance table,
PACKAGE/src/apps/translations/LOCALE.source-hashes.generated.ts: an entry exists exactly while that leaf is still a byte copy of the source revision.dataset.fields.measures.helpText, the one leaf here a translator had already written, carried none in any of them.After the translations,
pnpm i18n:extractdropped exactly 21 rows (7 keys × 3 locales), 0 added. The table now records these leaves as authored rather than copied.Per-leaf decisions
Every verdict below is
translate— the echo was an unauthored extractor fill. That is a conclusion per leaf, not a policy: the ledger'sechoverdict exists and costs one recorded sentence per locale, and no leaf here earned it.report.fields.dataset(label)blocks.datasetcarries authored text in all three locales — 数据集 / データセット / Conjunto de datos — landed deliberately by #19345. Note this settles the one case triage flagged as possibly legitimate (Datasetines-ES): this catalog's own authored answer for that word, in that locale, is "Conjunto de datos".report.fields.dataset(helpText)type,columns,order,chart) were authored.report.fields.values(label)blocks.valueson the same panel is authored 度量 / メジャー / Medidas.report.fields.values(helpText)order.helpTextnames the same two vocabularies (rows/columns dimensions, values measures) in authored text in every locale — the vocabulary existed; this leaf never got it.report.fields.rows(label)blocks.rowsis authored 行 / 行 / Filas, and the adjacent top-levelcolumnsis authored 列 / 列 / Columnas on the same panel.report.fields.rows(helpText)columns.helpText— the same sentence for the other axis — is authored in all three locales.dataset.fields.measures(label)helpTextin all three locales while itslabelwas a byte copy. One leaf cannot be a decision and a fill at once.dataset.fields.measures(helpText)The shared shape of the first seven: a translator who judged the English word right at the top level would not have written the translated one a few entries below it, in the same catalog, on the same panel, for the same concept.
The pin question, answered
The derived pin is not widened. A listed ledger is added instead.
repeater-row-properties.test.tsderives its population from the forms, and that derivation is the reason it is good: a repeater enumerated tomorrow is red the day it lands. Measured on this base:A pin that lands red is not a pin, and widening it would also retro-scope #17508's carrier ruling. So the derived population stays as #17508 drew it, and the DECIDED leaves are enumerated in a new file whose header states the split. This is the ledger row the card asks for rather than prose: a future deliberate echo is a
verdict: 'echo'row that must carry its own per-locale reason, and the suite refuses one without it.Each row is also pinned to the
ensource it was decided against, so a reworded source reds the file instead of leaving a decision standing over text nobody judged.Reverse verification
Both mutations ran through
scripts/ablation-replace.mjs(anchor must hit; on-disk blob before/after; restore proven equal to theHEADblob withgit diff HEADempty). Baseline first: 7 passed.ensource (zh-CNreport.fields.values.label): blob34c532953acfto3f7269b0789b, run 1 failed / 6 passed,AssertionError: zh-CN report.fields.values.label reads its en source again. Restored, blob back to34c532953acf.ensource a ledger row is pinned to (ValuestoValue): blobb8a246889186to689bf01a679d, run 2 failed / 5 passed — the source pin AND the dark control (expected 7 to be 8), so the control is live too. Restored, blob back tob8a246889186.An earlier attempt at A is reported as a no-op rather than quietly re-run: the mutation and its restore trap sat in one shell that exited between them, so the file was already restored when the suite ran and the green it printed measured nothing.
The generated-bundle hazard the claim flagged
These bundles are generated and
git check-attr mergereadsunspecifiedfor all of them (control:packages/spec/spec-changes.jsonreadsos-regen), so two branches adding i18n keys would merge clean and drop one side. Nothing collides here — this change adds and removes no keys at all, only leaf values — and the bundles were regenerated with the repo's own tooling, never by hand:It rewrote all eleven bundles; the three translated
metadata-formsbundles round-tripped byte-identical to the hand-edited values, which is itself the evidence that the edits are exactly what the extractor's merge baseline produces.Changeset, measured
@objectstack/platform-objectsis notprivate,publishConfig.accessispublic, andfiles[]shipsdist. Built, then greppeddistfor a new string with a pre-existing translated leaf on the same panel as the positive control: both reach the same five published entry points (index.js,index.mjs,plugin.js,plugin.mjs,metadata-translations/index.*). A test-only string reaches none of them. ⇒ the diff publishes,skip-changesetwould be wrong, and apatchchangeset is included.Verification
Head
68049082b.pnpm --filter @objectstack/platform-objects test— 43 files, 599 tests passed (was 43/592 with the new file's 7 tests added; the translations directory alone goes 9 files/108 tests to 10/115).pnpm --filter @objectstack/platform-objects typecheck— green, includingcheck:test-typecheck. The new test file is intsconfig.test.json's program (counted with--listFiles) and, by design, not in the main one.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— 59 derived, 59 run, 0 NOT-MEASURED, every one recorded with its exit code. Includescheck:i18n,check:i18n-stale-fill,check:nul-bytes,check:published-files,check:type-check-coverage,check:type-check-debt,check:cross-package-test-inputs,check:test-source-alias.check:i18n-coverage(13 configs, 621 baselined, none new) andcheck:i18n-walk-parity— both green.pnpm lint— the whole repo, exit 0, no output.check:dual-build-cjs-loadsansweredPREREQUISITE NOT MET(exit 3) on its first run because twelve packages had nodistyet. That is not a red and was not recorded as one; once the workspace was fully built it ran for real and passed (104 entry points across 67 packages).Acceptance notes
Noted, not filed — observations, no defect named:
datasetpanel's ownlabel,descriptionand all foursectionsare stillenechoes in all three locales, as are seven further top-leveldataset.fields.*keys. Same class as this card, outside its four keys.report.sections.dataset_binding(label and description) andreport.fields.drilldown/runtimeFilterare echoes on the panel this change touches.To file (reproducible defect, class (a)) — handed to the seat, not filed by this PR:
3ff6ddf1eover all 417 declared form-field paths; 0 of them are insiderepeater-row-properties.test.ts's 124-property population, so no pin sees them. A user in a translated locale reads English on those keys. It is the same class as this card and explicitly not mechanical — each key needs the same per-leaf judgement — so it is not taken in-place here. Dedupe words:en echo translated locale·metadata form field label untranslated·dataset.fields.dimensions object.fields.enable page.fields.interfaceConfig·declared form field echo census·translated leaf copy of source.Generated by Claude Code